/*
 * Jamus
 * Copyright (C) 2010 Jamus Team
 *
 * Jamus is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Jamus is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Jamus.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <mpd/client.h>
#include <boost/date_time.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/thread.hpp>

#include "Plugin.h"
#include "../../NoCurrentTrackException.h"

using namespace std;
using namespace boost::asio;

namespace backend {
namespace mpd {

log4cxx::LoggerPtr Plugin::logger(log4cxx::Logger::getLogger("backend.mpd.Plugin"));

namespace {
	void buildTrackFromMpdSong(const mpd_song* song, Track& track)
	{
		track.setFileName(mpd_song_get_uri(song));

		int duration = mpd_song_get_duration(song);
		if(duration)
			track.setDuration(boost::posix_time::seconds(duration));

		const char* artist = mpd_song_get_tag(song, MPD_TAG_ARTIST, 0);
		if(artist)
			track.setArtist(artist);

		const char* album = mpd_song_get_tag(song, MPD_TAG_ALBUM, 0);
		if(album)
			track.setAlbum(album);

		const char* title = mpd_song_get_tag(song, MPD_TAG_TITLE, 0);
		if(title)
			track.setTitle(title);

		const char* trackNum = mpd_song_get_tag(song, MPD_TAG_TRACK, 0);
		if(trackNum) {
			string tagValue(trackNum);
			// tagValue has format "trackNumber/numTotalTracks"
			int slashPos = tagValue.find('/');
			try {
				track.setTrackNumber(boost::lexical_cast<int>(tagValue.substr(0, slashPos)));
			} catch(boost::bad_lexical_cast&) {}
		}
	}
}

Plugin::Plugin(const string& host, int port, int timeout)
: running(false),
  status(0),
  isIdle(false),
  isIdleAllowed(true),
  numLocksWaiting(0)
{
	// Connect to MPD
	mpd = mpd_connection_new(host.c_str(), port, timeout * 1000);
	assert(mpd);

	if(mpd_connection_get_error(mpd)) {
		LOG4CXX_ERROR(logger, "Could not connect to the MPD server: " << mpd_connection_get_error_message(mpd));
		throw runtime_error(mpd_connection_get_error_message(mpd));
	}

	const unsigned int* version = mpd_connection_get_server_version(mpd);
	LOG4CXX_INFO(logger, "Connected to MPD server. Version: " << version[0] << "." << version[1] << "." << version[2]);

	// Get status
	fetchStatusFromServer();

	// Get list of all tracks
	// XXX: This is inefficient, optimization is needed. It is probably
	// reasonable to fetch info on demand.
	mpd_send_list_all_meta(mpd, 0);

	if(mpd_connection_get_error(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));

	for(mpd_entity* entity = mpd_recv_entity(mpd); entity; entity = mpd_recv_entity(mpd)) {
		if(mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG) {
			const mpd_song* song = mpd_entity_get_song(entity);
			Track track;
			buildTrackFromMpdSong(song, track);
			tracks.push_back(track);
		}

		mpd_entity_free(entity);
	}

	if(mpd_connection_get_error(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));

	// Now on to the current playlist
	fetchPlaylistFromServer();

	LOG4CXX_INFO(logger, "MPD back-end ready.");
}

Plugin::~Plugin()
{
	mpd_connection_free(mpd);
}

void Plugin::run()
{
	running = true;

	while(running) {
		// Wait until idle is allowed, then enter idle mode
		{
			boost::unique_lock<boost::mutex> lock(mutex);

			while(!isIdleAllowed)
				idleAllowed.wait(lock);

			// If cancel() was called while waiting, we would get an error in mpd_send_idle, so we need to check for this
			if(!running)
				break;

			assert(!isIdle);

			if(!mpd_send_idle(mpd))
				throw runtime_error(mpd_connection_get_error_message(mpd));

			isIdle = true;
		}

		// Wait until there is data to be read
		struct pollfd fd;
		fd.fd = mpd_connection_get_fd(mpd);
		fd.events = POLLIN;

		if(poll(&fd, 1, -1) != 1)
			throw runtime_error(strerror(errno));

		// Read result, then exit idle mode and notify other threads
		mpd_idle result;
		{
			boost::lock_guard<boost::mutex> lock(mutex);
			result = mpd_recv_idle(mpd, true);
			isIdle = false;
			idleFinished.notify_all();
		}

		// Process result
		if(!result) {
			// This might be an error...
			if(mpd_connection_get_error(mpd))
				throw runtime_error(mpd_connection_get_error_message(mpd));

			// ... or it's because idle mode was aborted (e. g. cancel())
			continue;
		}

		// First update this object's state before notifying others because
		// they could invoke this object which does not have completely
		// updated its state by then
		if(result & MPD_IDLE_QUEUE) {
			LOG4CXX_TRACE(logger, "Playlist changed, fetching new playlist and status");
			fetchPlaylistFromServer();

			// We also need to fetch a new status because playlist positions have changed
			fetchStatusFromServer();
		}

		if(result & MPD_IDLE_PLAYER) {
			// What's the new playback status?
			LOG4CXX_TRACE(logger, "Playback status changed, fetching new status");
			fetchStatusFromServer();
		}

		if(result & MPD_IDLE_MIXER) {
			// What's the new volume?
			LOG4CXX_TRACE(logger, "Mixer changed, fetching new status");
			fetchStatusFromServer();
		}

		// Now notify listeners about the changes
		if(result & MPD_IDLE_QUEUE) {
			LOG4CXX_TRACE(logger, "Emitting sigPlaylistChanged");
			sigPlaylistChanged();
		}

		if(result & MPD_IDLE_PLAYER) {
			LOG4CXX_TRACE(logger, "Emitting sigPlaybackStatusChanged");
			sigPlaybackStatusChanged();
		}

		if(result & MPD_IDLE_MIXER) {
			LOG4CXX_TRACE(logger, "Emitting sigVolumeChanged");
			sigVolumeChanged();
		}
	}

	LOG4CXX_TRACE(logger, "run() is done");
}

void Plugin::cancel()
{
	Lock lock(*this);
	
	running = false;
	
	if(!mpd_send_noidle(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

Plugin::PlaybackStatus Plugin::getPlaybackStatus()
{
	Lock lock(*this);

	switch(mpd_status_get_state(status)) {
		case MPD_STATE_STOP:
			return STOP;
		case MPD_STATE_PLAY:
			return PLAY;
		case MPD_STATE_PAUSE:
			return PAUSE;
		default:
			LOG4CXX_WARN(logger, "Unknown MPD player state");
			break;
	}

	// Help!
	return STOP;
}

void Plugin::play()
{
	Lock lock(*this);

	if(!mpd_run_play(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

void Plugin::play(int track)
{
	Lock lock(*this);

	if(!mpd_run_play_pos(mpd, track))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

void Plugin::pause()
{
	Lock lock(*this);

	if(!mpd_run_pause(mpd, true))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

void Plugin::playPause()
{
	Lock lock(*this);

	if(!mpd_run_toggle_pause(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

void Plugin::stop()
{
	Lock lock(*this);

	if(!mpd_run_stop(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

void Plugin::nextTrack()
{
	Lock lock(*this);

	if(!mpd_run_next(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

void Plugin::prevTrack()
{
	Lock lock(*this);

	if(!mpd_run_previous(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

int Plugin::getCurrentTrack()
{
	Lock lock(*this);

	assert(status);

	int n = mpd_status_get_song_pos(status);

	if(n < 0)
		throw NoCurrentTrackException();

	return n;
}

boost::posix_time::time_duration Plugin::getElapsed()
{
	Lock lock(*this);

	assert(status);

	if(mpd_status_get_song_pos(status) < 0)
		throw NoCurrentTrackException();

	assert(! (elapsedOnLastUpdate.is_special() || timeOfLastUpdate.is_special()));

	if(mpd_status_get_state(status) != MPD_STATE_PLAY)
		return elapsedOnLastUpdate;

	return elapsedOnLastUpdate + (boost::posix_time::microsec_clock::local_time() - timeOfLastUpdate);
}

void Plugin::seek(const boost::posix_time::time_duration& pos)
{
	Lock lock(*this);

	const unsigned int current = mpd_status_get_song_pos(status);

	if(current < 0)
		throw NoCurrentTrackException();

	mpd_run_seek_pos(mpd, current, pos.total_seconds());
}

int Plugin::getVolume()
{
	Lock lock(*this);

	assert(status);

	int vol = mpd_status_get_volume(status);

	if(vol < 0) {
		LOG4CXX_WARN(logger, "Volume not supported");
		return 0;
	}

	assert(vol <= 100);
	return vol;
}

void Plugin::setVolume(int vol)
{
	Lock lock(*this);

	assert(0 <= vol && vol <= 100);
	if(!mpd_run_set_volume(mpd, vol))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

const Plugin::TrackList& Plugin::getTrackList()
{
	Lock lock(*this);

	return tracks;
}

const Plugin::Playlist& Plugin::getPlaylist()
{
	Lock lock(*this);

	return playlist;
}

void Plugin::addToPlaylist(const std::string& fileName)
{
	Lock lock(*this);

	if(!mpd_run_add(mpd, fileName.c_str()))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

void Plugin::removeFromPlaylist(int index)
{
	Lock lock(*this);

	LOG4CXX_TRACE(logger, "Trying to remove playlist entry number " << index);
	if(!mpd_run_delete(mpd, index))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

void Plugin::clearPlaylist()
{
	Lock lock(*this);

	if(!mpd_run_clear(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

void Plugin::fetchPlaylistFromServer()
{
	Lock lock(*this);

	mpd_send_list_queue_meta(mpd);

	if(mpd_connection_get_error(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));

	playlist.clear();

	for(mpd_entity* entity = mpd_recv_entity(mpd); entity; entity = mpd_recv_entity(mpd)) {
		assert(mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG);
		const mpd_song* song = mpd_entity_get_song(entity);
		Track track;
		buildTrackFromMpdSong(song, track);
		playlist.push_back(track);

		mpd_entity_free(entity);
	}

	if(mpd_connection_get_error(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));

	if(!mpd_response_finish(mpd))
		throw runtime_error(mpd_connection_get_error_message(mpd));
}

void Plugin::fetchStatusFromServer()
{
	Lock lock(*this);

	if(status)
		mpd_status_free(status);

	status = mpd_run_status(mpd);

	if(!status)
		throw runtime_error(mpd_connection_get_error_message(mpd));

	// Update elapsed time
	elapsedOnLastUpdate = boost::posix_time::milliseconds(mpd_status_get_elapsed_ms(status));
	timeOfLastUpdate =  boost::posix_time::microsec_clock::local_time();
}

Plugin::Lock::Lock(Plugin& p)
: p(p), lock(p.mutex)
{
	p.isIdleAllowed = false;

	if(p.isIdle) {
		if(!mpd_send_noidle(p.mpd))
			throw runtime_error(mpd_connection_get_error_message(p.mpd));

		// Wait until not idle anymore
		++p.numLocksWaiting;
		do {
			p.idleFinished.wait(lock);
		} while(p.isIdle);
		--p.numLocksWaiting;
	}

	assert(!p.isIdle);
}

Plugin::Lock::~Lock()
{
	// Notify idle thread
	if(p.numLocksWaiting == 0) {
		p.isIdleAllowed = true;
		p.idleAllowed.notify_one();
	}
}

} // namespace mpd
} // namespace backend

extern "C" backend::mpd::Plugin* create(const boost::property_tree::ptree& config)
{
	return new backend::mpd::Plugin(
			config.get("config.backend.mpd.host", "127.0.0.1"),
			config.get("config.backend.mpd.port", 6600),
			config.get("config.backend.mpd.timeout", 5));
}

extern "C" void destroy(backend::mpd::Plugin* p)
{
	delete p;
}
